# Auto-fill gaps: A REAPER extension to fill gaps automatically
# Copyright (C) 2011 by George Giaslas
# Contact email: ggiaslas@gmail.com
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#


import re
import math
import io
import configparser
import base64
import platform
from reaper_python import *


####################
# global variables #
####################

trackList = []

# refresh the track list
def updateTrackList():
  global trackList
  trackList = []
total = RPR_CountTracks(0)
for i in range(0, total):
  trackList.append(RPR_GetTrack(0, i))
  return

updateTrackList()

itemInfoCachingEnabled = False
itemInfo = {}
HEAD_KEY = 'head'
LENGTH_KEY = 'len'
TRACK_KEY = 'track'
GROUPID_KEY = 'groupId'
FIRST_TAKE_KEY = 'firstTake'


###########
# methods #
###########

# return true if diff of floating point is less than 1^-10
def areEqual(doubleA, doubleB):
  return math.fabs(doubleA - doubleB) < 0.0000000001


# return true if diff of floating point is more than 1^-10
def isAGreaterThanB(doubleA, doubleB):
  return (doubleA - doubleB) > 0.0000000001


# enables the item info caching in order to limit API calls
def enableItemInfoCaching():
  global itemInfoCachingEnabled
  itemInfoCachingEnabled = True
  return
  

# return item's start position
def getHeadPosition(item):
  if itemInfoCachingEnabled:
    global itemInfo
    itemStr = str(item)
    if not itemStr in itemInfo:
      itemInfo[itemStr] = {}
    info = itemInfo[itemStr]
    if HEAD_KEY in info:
      return info[HEAD_KEY]
    else:
      pos = _getHeadPosition(item)
      info[HEAD_KEY] = pos
      return pos
  else:
    return _getHeadPosition(item)


def _getHeadPosition(item):
  return RPR_GetMediaItemInfo_Value(item, "D_POSITION")


# set item's head position
def setHeadPosition(item, pos):
  RPR_SetMediaItemInfo_Value(item, "D_POSITION", pos)
  
  if itemInfoCachingEnabled:
    global itemInfo
    itemStr = str(item)
    if not itemStr in itemInfo:
      itemInfo[itemStr] = {}
    info = itemInfo[itemStr]
    info[HEAD_KEY] = pos
  
  return

# return item's length
def getLength(item):
  if itemInfoCachingEnabled:
    global itemInfo
    itemStr = str(item)
    if not itemStr in itemInfo:
      itemInfo[itemStr] = {}
    info = itemInfo[itemStr]
    if LENGTH_KEY in info:
      return info[LENGTH_KEY]
    else:
      length = _getLength(item)
      info[LENGTH_KEY] = length
      return length
  else:
    return _getLength(item)

def _getLength(item):
  return RPR_GetMediaItemInfo_Value(item, "D_LENGTH")


# set item's new length
def setLength(item, length):
  RPR_SetMediaItemInfo_Value(item, "D_LENGTH", length)
  
  if itemInfoCachingEnabled:
    global itemInfo
    itemStr = str(item)
    if not itemStr in itemInfo:
      itemInfo[itemStr] = {}
    info = itemInfo[itemStr]
    info[LENGTH_KEY] = length
  
  return

# return item's end position
def getTailPosition(item):
  return getHeadPosition(item) + getLength(item)


# crop item's tail by decreasing its length by specified msec
def cropTail(item, msec):
  extendTail(item, -msec)
  return


# extend item's tail by increasing its length by specified msec
def extendTail(item, msec):
  oldLength = getLength(item)	# seconds
  newLength = oldLength + (msec/1000.0)
  setLength(item, newLength)
  return


# crop item's head by increasing its start & source position and decreasing its length by specified msec
def cropHead(item, msec):
  extendHead(item, -msec)
  return


# extend item's head by decreasing its start & source position and increasing length by specified msec
def extendHead(item, msec):
  oldSource = getSlipOffset(item) # seconds
  oldStart = getHeadPosition(item)	# seconds
  oldLength = getLength(item)	# seconds
  
  newSource = oldSource - (msec/1000.0)
  newStart = oldStart - (msec/1000.0)
  newLength = oldLength + (msec/1000.0)
  
  setSlipOffset(item, newSource)
  setHeadPosition(item, newStart)
  setLength(item, newLength)
  return


# set item's snap offset to specified relative position in msec
def setSnapOffset(item, msec):
  RPR_SetMediaItemInfo_Value(item, "D_SNAPOFFSET", msec/1000.0)
  return


# set item's fade in to specified length in msec
def setFadeIn(item, msec, type):
  RPR_SetMediaItemInfo_Value(item, "D_FADEINLEN", msec/1000.0)
  RPR_SetMediaItemInfo_Value(item, "D_FADEINLEN_AUTO", -1)
  RPR_SetMediaItemInfo_Value(item, "C_FADEINSHAPE", type)
  return


# set item's fade out to specified length in msec
def setFadeOut(item, msec, type):
  RPR_SetMediaItemInfo_Value(item, "D_FADEOUTLEN", msec/1000.0)
  RPR_SetMediaItemInfo_Value(item, "D_FADEOUTLEN_AUTO", -1)
  RPR_SetMediaItemInfo_Value(item, "C_FADEOUTSHAPE", type)
  return


# return item's first take
def getFirstTake(item):
  if itemInfoCachingEnabled:
    global itemInfo
    itemStr = str(item)
    if not itemStr in itemInfo:
      itemInfo[itemStr] = {}
    info = itemInfo[itemStr]
    if FIRST_TAKE_KEY in info:
      return info[FIRST_TAKE_KEY]
    else:
      take0 = _getFirstTake(item)
      info[FIRST_TAKE_KEY] = take0
      return take0
  else:
    return _getFirstTake(item)

def _getFirstTake(item):
  return RPR_GetMediaItemTake(item, 0)
  

# return item's playrate
def getPlayrate(item):
  # get first take's playrate
  take0 = RPR_GetMediaItemTake(item, 0)
  playrateFloat = RPR_GetMediaItemTakeInfo_Value(take0, "D_PLAYRATE")
  
  return playrateFloat


# set item's playrate & update it's length to adapt new playrate
def setPlayrate(item, newRate):
  # set first take's playrate
  take0 = RPR_GetMediaItemTake(item, 0)
  RPR_SetMediaItemTakeInfo_Value(take0, "D_PLAYRATE", newRate)

  # extend tail to adapt to new playrate
  curLen = getLength(item)
  newLen = curLen / newRate
  tailToExtend = (newLen - curLen) * 1000.0
  extendTail(item, tailToExtend)
  
  return


# return item's slip offset
def getSlipOffset(item):
  # get first take's slip offset
  take0 = RPR_GetMediaItemTake(item, 0)
  startInSourceFloat = RPR_GetMediaItemTakeInfo_Value(take0, "D_STARTOFFS")
  
  return startInSourceFloat


# set item's slip offset
def setSlipOffset(item, newPos): 
  # set first take's slip offset
  take0 = RPR_GetMediaItemTake(item, 0)
  RPR_SetMediaItemTakeInfo_Value(take0, "D_STARTOFFS", newPos)
		
  return


# return edit cursor position
def getCursor():
  return RPR_GetCursorPositionEx(0)


# set edit cursor position
def setCursor(pos):
  return RPR_SetEditCurPos2(0, pos, False, False)


# return position of the first transient in group of items
# and optionally limit search only to the 'searchInFirstMsec' msec
# returns -1 if no transient exists
def firstTransientInGroup(group, searchInFirstMsec):
  if len(group) == 0:
    return -1
  
  # select items & move cursor to the head
  # also zoom in to cursor so transient is properly detected
  headPos = getHeadPosition(group[0])
  exclusiveSelectGroup(group)
  setCursor(headPos)
  RPR_adjustZoom(500, 1, False, 1)
  
  # call action to move cursor to next transient
  RPR_Main_OnCommand(40375, 0)
  
  # if cursor falls inside the item and in the limit specified
  # then return the transient position
  # if cursor doesn't move at all then check will fail since
  # we're checking for pos greater than head position
  transientPos = getCursor()
  searchToPos = getTailPosition(group[0])
  if searchInFirstMsec != -1:
    searchToPos = min(headPos + (searchInFirstMsec/1000.0), searchToPos)
  if (transientPos > headPos) and (transientPos < searchToPos):
    return transientPos

  return -1


# return position of the first transient in a single item
# and optionally limit search only to the 'searchInFirstMsec' msec
# returns -1 if no transient exists
def firstTransientInItem(item, searchInFirstMsec):
  return firstTransientInGroup([item], searchInFirstMsec)

# return position of the last transient in group of items
# and optionally skip the last 'skipLastMsec' so start searching before this point
# or -1 if no transient exists
def lastTransientInGroup(group, skipLastMsec):
  if len(group) == 0:
    return -1

  # select item & move cursor to the end minus skipLastMsec
  # also zoom in to cursor so transient is properly detected
  tailPos = getTailPosition(group[0]) - (skipLastMsec/1000.0)
  if (tailPos < getHeadPosition(group[0])) or (tailPos > getTailPosition(group[0])):
    return -1
  exclusiveSelectGroup(group)
  setCursor(tailPos)
  RPR_adjustZoom(500, 1, False, 1)
  
  # call action to move cursor to previous transient
  RPR_Main_OnCommand(40376, 0)
  
  # if cursor falls inside the item then return the transient position
  # if cursor doesn't move at all then check will fail since
  # we're checking for pos less than tail
  transientPos = getCursor()
  if (transientPos >= getHeadPosition(group[0])) and (transientPos < tailPos):
    return transientPos

  return -1


# return position of the last transient in single item
# and optionally skip the last 'skipLastMsec' so start searching before this
# or -1 if no transient exists
def lastTransientInItem(item, skipLastMsec):
  return lastTransientInGroup([item], skipLastMsec)


# split item at specified position adding a crossfade on the right of the split
# returns the right-hand split
def splitItemWithCrossfadeOnRight(item, pos, xfadeMsec, xfadeType):
  # split item
  newItem = RPR_SplitMediaItem(item, pos)
  
  # update item's length after split if caching item info
  if itemInfoCachingEnabled:
    global itemInfo
    itemStr = str(item)
    if itemStr in itemInfo:
      info = itemInfo[itemStr]
      if LENGTH_KEY in info:
        length = _getLength(item)
        info[LENGTH_KEY] = length
  
  # overlap old with new
  if xfadeMsec > 0:
    extendTail(item, xfadeMsec)
  
    # set transient split crossfade
    setFadeOut(item, xfadeMsec, xfadeType)
    setFadeIn(newItem, xfadeMsec, xfadeType)
            
  return newItem


# split group of items at specified position adding a crossfade on the right of the split
# returns a list with all right-hand splits
def splitGroupWithCrossfadeOnRight(group, pos, xfadeMsec, xfadeType):
  newSplits = []
  
  for i in range(0, len(group)):
    newItem = splitItemWithCrossfadeOnRight(group[i], pos, xfadeMsec, xfadeType)
    newSplits.append(newItem)
  
  # group right-hand splits
  if len(newSplits) > 0:
    exclusiveSelectGroup(newSplits)
    RPR_Main_OnCommand(40032, 0) # group selected items

  return newSplits


# select/deselect the media item
def setSelected(item, selected):
  RPR_SetMediaItemInfo_Value(item, "B_UISEL", selected)

  return
  

# select only the specified media item
def exclusiveSelectItem(item):
  unselectAll()
  setSelected(item, True)

  return


# select only the specified media items in group
def exclusiveSelectGroup(group):
  unselectAll()
  for i in range(0, len(group)):
    setSelected(group[i], True)

  return


# return item's group id - 0 for no group
def getGroupId(item):
  if itemInfoCachingEnabled:
    global itemInfo
    itemStr = str(item)
    if not itemStr in itemInfo:
      itemInfo[itemStr] = {}
    info = itemInfo[itemStr]
    if GROUPID_KEY in info:
      return info[GROUPID_KEY]
    else:
      groupid = _getGroupId(item)
      info[GROUPID_KEY] = groupid
      return groupid
  else:
    return _getGroupId(item)

def _getGroupId(item):
  return RPR_GetMediaItemInfo_Value(item, "I_GROUPID")


# return item's track
def getTrack(item):
  if itemInfoCachingEnabled:
    global itemInfo
    itemStr = str(item)
    if not itemStr in itemInfo:
      itemInfo[itemStr] = {}
    info = itemInfo[itemStr]
    if TRACK_KEY in info:
      return info[TRACK_KEY]
    else:
      track = _getTrack(item)
      info[TRACK_KEY] = track
      return track
  else:
    return _getTrack(item)

def _getTrack(item):
  return RPR_GetMediaItem_Track(item)
  
  
# return item's track number (1-based)
# or -1 if track not found
def getTrackNumber(track):
  total = len(trackList)
  for i in range(0, total):
    if track == trackList[i]:
      return i+1
  return -1


# return track from track number (1-based)
# or False if number not found
def getTrackFromNumber(number):
  n = int(number) - 1
  if n < 0 or n >= len(trackList):
    return False
  
  return trackList[n]


# return list containing name of tracks
def getTrackNames():
  names = []
  total = len(trackList)
  for i in range(0, total):
    (name, track, flags) = RPR_GetTrackState(trackList[i], 0)
    if len(name) > 0:
      names.append(name)
    else:
      names.append("<no name>")
  return names


# return total number of tracks
def getTotalTracks():
  return len(trackList)


# delete the specified track number (1-based)
def deleteTrack(number):
  RPR_DeleteTrack(getTrackFromNumber(number))
  return


# return True if items are in different tracks
# and have the same group id and the same start & length
def areGrouped(item1, item2):
  if item1 == item2:
    return False

  if getTrack(item1) == getTrack(item2):
    return False
  
  group1 = getGroupId(item1)
  if group1 == 0:
    return False

  group2 = getGroupId(item2)
  if group2 == 0:
    return False

  return group1 == group2 and areEqual(getHeadPosition(item1), getHeadPosition(item2)) and areEqual(getLength(item1), getLength(item2))
  

# unselect all media items
def unselectAll():
  RPR_Main_OnCommand(40289, 0)
  return


# select all items in project
def selectAll():
  RPR_Main_OnCommand(40182, 0)
  return


# select items in time selection
def selectItemsInTimeSelection():
  RPR_Main_OnCommand(40717, 0)
  return


# unselect all tracks
def unselectAllTracks():
  RPR_Main_OnCommand(40297, 0)
  return


# select all tracks
def selectAllTracks():
  RPR_Main_OnCommand(40296, 0)
  return
  
  
# select only specified track
def exclusiveSelectTrack(trackIdx):
  unselectAllTracks()
  RPR_SetTrackSelected(getTrackFromNumber(trackIdx), True)
  return


# validate the given groups. they should have equal number of items
# and 1-1 corresponce in the same tracks
def validateGroups(group1, group2):
  if len(group1) != len(group2):
    return False

  for i in range(0, len(group1)):
    if getTrack(group1[i]) != getTrack(group2[i]):
      return False

  return True


# returns only items that belong to the tracks specified
def filterItemsByTrack(items, tracks):
  ret = []
  for i in range(0, len(items)):
    if getTrack(items[i]) in tracks:
      ret.append(items[i])

  return ret


# create a marker at the specified position
def createMarkerAt(position, title):
  RPR_AddProjectMarker(0, False, position, 0, title, 1);
  return


# message box methods
def dbg(str):
  RPR_ShowMessageBox(str, "debug", 0)
  return


def error(str):
  RPR_ShowMessageBox(str, "error", 0)
  return

def alert(title,str):
  RPR_ShowMessageBox(str, title, 0)
  return


# get input from user passing list of names/types/default values, all in csv
# types supported are: 's'->string, 'i'->integer, 'f'->float, 'b'->boolean
# returns tuple of returned values
def enterParameters(title, names, types, dvalues):
  # call dialog and get result
  nitems = len(names.split(","))
  res = RPR_GetUserInputs(title, nitems, names, dvalues, 1024)
  
  # abort if 'Cancel' was pressed
  if not res[0]:
    return False

  # the fifth item holds the input values
  resvalues = res[4].split(',')

  # convert string values to requested data types
  ret = []
  typesList = types.split(',')
  for i in range(0, nitems):
    if typesList[i] == 's':
      ret.append(resvalues[i])
    elif typesList[i] == 'i':
      ret.append(int(float(resvalues[i])))
    elif typesList[i] == 'f':
      ret.append(float(resvalues[i]))
    elif typesList[i] == 'b':
      if resvalues[i].lower() == 'true' or resvalues[i] == '1':
        ret.append(True)
      else:
        ret.append(False)
  
  return tuple(ret)


# read parameter values from ini file
# if some value does not exist return its default value
# returns 0 if ini file does not exist
def readParameters(file, section, names, types, defaults):
  # open file and read config
  fullPath = RPR_GetResourcePath() + '\\' + file
  if not RPR_file_exists(fullPath):
    return False
  cfg = configparser.ConfigParser()
  cfg.read(fullPath)
  
  if not cfg.has_section(section):
    return False
  
  # read and convert values to requested data types
  ret = []
  namesList = names.split(',')
  typesList = types.split(',')
  defaultsList = defaults.split(',')
  for i in range(0, len(namesList)):
    option = namesList[i].lower()
    if cfg.has_option(section, option):
      if typesList[i] == 's':
        ret.append(cfg.get(section, option))
      elif typesList[i] == 'i':
        ret.append(cfg.getint(section, option))
      elif typesList[i] == 'f':
        ret.append(cfg.getfloat(section, option))
      elif typesList[i] == 'b':
        ret.append(cfg.getboolean(section, option))
    else:
      ret.append(defaultsList[i])
  
  return tuple(ret)

 
# write parameter values to ini file
# and create if not exists already
def writeParameters(file, section, names, values):
  # open/create file & read config if file already exists
  fullPath = RPR_GetResourcePath() + '\\' + file
  cfg = configparser.ConfigParser()
  if RPR_file_exists(fullPath):
    cfg.read(fullPath)
  
  # write values to config
  namesList = names.split(',')
  valuesList = values.split(',')
  if not cfg.has_section(section):
    cfg.add_section(section)
  for i in range(0, len(namesList)):
    cfg.set(section, namesList[i].lower(), valuesList[i])
  
  # write & close file
  cfgFile = open(fullPath, 'w')
  cfg.write(cfgFile)
  cfgFile.close()

  return


# return the value of the specified reaper setting
def getReaperSetting(var, type):
  # open file and read config
  fullPath = RPR_get_ini_file()
  if not RPR_file_exists(fullPath):
    return False
  
  cfg = configparser.ConfigParser()
  cfg.read(fullPath)
  
  # read and convert values to requested data types
  # read from the 'REAPER' or 'reaper' section
  section = ""
  if cfg.has_section("REAPER"):
    section = "REAPER"
  elif cfg.has_section("reaper"):
    section = "reaper"
  else:
    return False

  if type == 's':
    return cfg.get(section, var)
  elif type == 'i':
    return cfg.getint(section, var)
  elif type == 'f':
    return cfg.getfloat(section, var)
  elif type == 'b':
    return cfg.getboolean(section, var)
  
  return False


# return the value of 'overlap & crossfade on split' setting
def getOverlapAndCrossfadeOnSplit():
  v = getReaperSetting("splitautoxfade", "i")
  return (v & 0x01) > 0


# set the value of 'overlap & crossfade on split' setting
def setOverlapAndCrossfadeOnSplit(v):
  if getOverlapAndCrossfadeOnSplit() != v:
   RPR_Main_OnCommand(40912, 0)
  return


# convert string to base64 without new lines
def convertToBase64(s):
  return str(base64.b64encode(s.encode()).decode()).replace("\r", "").replace("\n", "")


# check for running platform
def isWindowsPlatform():
  return platform.system() == 'Windows'

